/*
 * Dijjer - A Peer to Peer HTTP Cache
 * Copyright (C) 2004,2005 Change.Tv, Inc, Mathias De Maré
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package dijjer.updater;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import dijjer.AbstractFactory;
import dijjer.io.comm.AbstractPeer;
import dijjer.io.comm.Message;
import dijjer.io.comm.WebPeer;
import dijjer.util.AbstractThread;
import dijjer.util.logging.Logger;

public class WebResourceManager extends AbstractResourceManager {
	
	private static final Map _UDPSockets = Collections.synchronizedMap(new HashMap());
	private static final Map _MCSockets = Collections.synchronizedMap(new HashMap());	
	
	
	public WebResourceManager() {
		mcastPort = 9113;
		try {
			String mcast = "239.250.240.230";
			InetAddress.getByName(mcast);
			mcastAddr = mcast;
		} catch (UnknownHostException e) {
			mcastAddr = null;
		}
	}

	public File downloadCodeBase() throws Exception {
		File jar = new File(_classCacheDirectory, System.currentTimeMillis() + ".dijjer.jar");
		Logger.debug("Downloading from " + _downloadURL + " to " + jar);

		BufferedInputStream in = new BufferedInputStream(new URL(_downloadURL).openConnection().getInputStream());
		FileOutputStream out = new FileOutputStream(jar, false);

		byte[] bytes = new byte[1024];
		int c = -1;
		while ((c = in.read(bytes)) != -1) {
			out.write(bytes, 0, c);
		}

		in.close();
		out.close();

		return jar;
	}
	
	public void createUDPSocket(int port) {
		checkShutdown();
		synchronized (_UDPSockets) {
			if (!_UDPSockets.containsKey(new Integer(port))) {
				try {
					_UDPSockets.put(new Integer(port), new UDPSocket(port) {
							{
							startThread();
							}
							});
				} catch (Exception e) {
					Logger.fatal(-1, "Couldn't connect to UDP port " + port + 
					                 ", is another instance of running on the same port?");
				}
			}
		}
	}
	
	public void createMCSocket(int port) {
		checkShutdown();
		synchronized (_MCSockets) {
			if (!_MCSockets.containsKey(new Integer(port))) {
				try {
					_MCSockets.put(new Integer(port), new MCSocket(port) {
						{
							startThread();
						}
					});
				} catch (Exception e) {
					Logger.debug("Couldn't connect to MC port " + port + ", is another instance of running on the same port?", e);
				}
			}
		}
	}

	public Message receive(int port) {
		createUDPSocket(port);
		UDPSocket socket = (UDPSocket) _UDPSockets.get(new Integer(port));
		DatagramPacket dp = socket.receive();
		if(dp == null)
			return null;
		Message m = null;
		try {
			m = Message.decodeFromPacket(dp);
		} catch (Exception e) {
			e.printStackTrace();
			Logger.error("Couldn't parse packet from " + dp.getAddress());
			return null;
		}
		return m;
	}

	public Message receiveMC(int port) {
		createMCSocket(port);
		MCSocket socket = (MCSocket) _MCSockets.get(new Integer(port));
		DatagramPacket dp = socket.receive();
		if(dp == null)
			return null;
		Message m = null;
		try {
			m = Message.decodeFromPacket(dp);
		} catch (Exception e) {
			e.printStackTrace();
			Logger.error("Couldn't parse packet from " + dp.getAddress());
			return null;
		}
		return m;
	}

	public void send(AbstractPeer destination, Message packet) {
		byte[] ba = packet.encodeToBytePacket();
		DatagramPacket dp = new DatagramPacket(ba, ba.length);
		WebPeer webDest = (WebPeer) destination;
		
		dp.setAddress(webDest.getAddress());
		dp.setPort(webDest.getPort());
		
		if (dp.getAddress().equals(mcastAddr)) {
			createMCSocket(mcastPort);
			MCSocket socket = (MCSocket) _MCSockets.get(new Integer(mcastPort));			
			socket.send(dp);
		} else {
			createUDPSocket(webDest.getPort());
			UDPSocket socket = (UDPSocket) _UDPSockets.get(new Integer(webDest.getPort()));
			socket.send(dp);
		}
	}

	public boolean validAddress(String addr) {
		InetAddress inetAddr = null;
		try {
			inetAddr = InetAddress.getByName(addr);
		}
		catch(UnknownHostException e) {
			Logger.warning("Could not find address", e);
			return false;
		}
        if (inetAddr.isAnyLocalAddress() || inetAddr.isLoopbackAddress()) {
            return true;
        }
        return _validAddresses.contains(inetAddr.getHostName())
            || _validAddresses.contains(inetAddr.getHostAddress());
	}
	
	public String getUDPSocketString(int port) {
		createUDPSocket(port);
		UDPSocket socket = (UDPSocket) _UDPSockets.get(new Integer(port));
		if (socket != null) {
		   return socket.toString();
		}
		return "UNBOUND";
	}
	
	public String getMCSocketString(int port) {
		createMCSocket(port);
		MCSocket socket = (MCSocket) _MCSockets.get(new Integer(port));
		if (socket != null) {
		   return socket.toString();
		}
		return "UNBOUND";
	}
	
	private class UDPSocket extends AbstractThread {

		private final DatagramSocket _socket;
		private final List _packets = new ArrayList(15);

		public UDPSocket(int port) throws Exception {
			long wait = 2;
			DatagramSocket socket = null;
			while (socket == null) {
				try {
					socket = new DatagramSocket(port);
				} catch (SocketException e) {
					Logger.warning("Could not bind to socket on port: " + port + ", trying again in " + wait + " seconds.");
					Thread.sleep(wait * 1000);
					wait *= 2;
				}
			}
			_socket = socket;
		}

		public DatagramPacket receive() {
			synchronized (_packets) {
				if (_packets.isEmpty() && !wasShutdown()) {
					checkShutdown();
					try {
						_packets.wait(1000);
					} catch (InterruptedException ex) {
						return null;
					}
				}
				if (_packets.isEmpty()) {
					return null;
				}
				return (DatagramPacket) _packets.remove(0);
			}
		}

		protected boolean loop() throws InterruptedException {
			DatagramPacket packet = new DatagramPacket(new byte[1500], 1500);
			try {
				_socket.receive(packet);
				synchronized (_packets) {
					_packets.add(packet);
					_packets.notifyAll();
				}

			} catch (SocketException e) {
				if (wasKilled() || wasShutdown()) {
					Logger.info("Shutting down UDP socket");
				} else {
					Logger.error("Problem receiving UDP request!", e);
				}
			} catch (IOException e) {
				Logger.error("Problem receiving UDP packet!", e);
			}
			return true;
		}

		public void send(DatagramPacket packet) {
			try {
				_socket.send(packet);
			} catch (Exception e) {
				Logger.error("Error while sending packet to " + packet.getSocketAddress(), e);
			}
		}

		protected void cleanUp() {
			if (!_packets.isEmpty()) {
				Logger.warning("Dumping "+ _packets.size() +" unused packets.");
			}
			_packets.clear();
		}

		protected void shutdown() {
			try {
				_socket.close();
			} catch (Exception expected) {
				Logger.debug("Junk exception: ", expected);
			}
		}

		public String toString() {
			return _socket.getLocalAddress() + ":" + _socket.getLocalPort();
		}
	}

	private class MCSocket extends AbstractThread {

		private final MulticastSocket _socket;
		private final List _packets = new ArrayList(15);

		public MCSocket(int port) throws Exception {
			long wait = 2;
			MulticastSocket socket = null;
			while (socket == null) {
				try {
					socket = new MulticastSocket(port);
					socket.joinGroup(InetAddress.getByName(mcastAddr));
					socket.setTimeToLive(1);
				} catch (SocketException e) {
					Logger.warning("Could not bind to socket on port: " + port + ", trying again in " + wait + " seconds.");
					Thread.sleep(wait * 1000);
					wait *= 2;
				}
			}
			_socket = socket;
		}

		public DatagramPacket receive() {
			synchronized (_packets) {
				if (_packets.isEmpty() && !wasShutdown()) {
					checkShutdown();
					try {
						_packets.wait(1000);
					} catch (InterruptedException ex) {
						return null;
					}
				}
				if (_packets.isEmpty()) {
					return null;
				}
				return (DatagramPacket) _packets.remove(0);
			}
		}

		protected boolean loop() throws InterruptedException {
			DatagramPacket packet = new DatagramPacket(new byte[1500], 1500);
			try {
				_socket.receive(packet);
				synchronized (_packets) {
					_packets.add(packet);
					_packets.notifyAll();
				}

			} catch (SocketException e) {
				if (wasKilled() || wasShutdown()) {
					Logger.info("Shutting down MC socket");
				} else {
					Logger.error("Problem receiving MC request!", e);
				}
			} catch (IOException e) {
				Logger.error("Problem receiving MC packet!", e);
			}
			return true;
		}

		public void send(DatagramPacket packet) {
			try {
				_socket.send(packet);
			} catch (Exception e) {
				Logger.error("Error while sending packet to " + packet.getSocketAddress(), e);
			}
		}

		protected void cleanUp() {
			if (!_packets.isEmpty()) {
				Logger.warning("Dumping "+ _packets.size() +" unused packets.");
			}
			_packets.clear();
		}

		protected void shutdown() {
			try {
				_socket.leaveGroup(InetAddress.getByName(AbstractFactory.getFactory().getResourceManager().mcastAddr));
			} catch (IOException e) {
				Logger.debug("Problem leaving multicast group: ", e);
			}
			
			try {
				_socket.close();
			} catch (Exception expected) {
				Logger.debug("Junk exception: ", expected);
			}
		}

		public String toString() {
			return _socket.getLocalAddress() + ":" + _socket.getLocalPort();
		}
	}

}
